package com.dianping.cat.local.integration.mybatis;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.sql.DataSource;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;

import com.alibaba.druid.pool.DruidDataSource;
import com.dianping.cat.Cat;
import com.dianping.cat.message.Message;
import com.dianping.cat.message.Transaction;
import com.zaxxer.hikari.HikariDataSource;

/**
 * <p>
 * 1.Cat-Mybatis plugin: Rewrite on the version of Steven;
 * <p>
 * 2.Support DruidDataSource,PooledDataSource(mybatis Self-contained data
 * source);
 * <p>
 * https://github.com/dianping/cat/tree/master/integration/mybatis
 * <p>
 * 
 * @see Mybatis-Plus 中的MybatisPlusInterceptor
 * 
 * @author zhanzehui(west_20@163.com)
 */

@Intercepts({
		@Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class,
				RowBounds.class, ResultHandler.class }),
		@Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class,
				RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class }),
		@Signature(method = "update", type = Executor.class, args = { MappedStatement.class, Object.class }) })
public class CatMybatisPlugin implements Interceptor {

	private static final Pattern PARAMETER_PATTERN = Pattern.compile("\\?");

	private static final String MYSQL_DEFAULT_URL = "jdbc:mysql://UUUUUKnown:3306/%s?useUnicode=true";

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		MappedStatement mappedStatement = this.getStatement(invocation);
		String methodName = this.getMethodName(mappedStatement);
		Transaction t = Cat.newTransaction("SQL", methodName);

		String sql = this.getSql(invocation, mappedStatement);
		SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
		Cat.logEvent("SQL.Method", sqlCommandType.name().toLowerCase(), Message.SUCCESS, sql);

		String url = this.getSQLDatabaseUrlByStatement(mappedStatement);
		Cat.logEvent("SQL.Database", url);

		return doFinish(invocation, t);
	}

	protected MappedStatement getStatement(Invocation invocation) {
		return (MappedStatement) invocation.getArgs()[0];
	}

	protected String getMethodName(MappedStatement mappedStatement) {
		String[] strArr = mappedStatement.getId().split("\\.");
		String methodName = strArr[strArr.length - 2] + "." + strArr[strArr.length - 1];

		return methodName;
	}

	protected String getSql(Invocation invocation, MappedStatement mappedStatement) {
		Object parameter = null;
		if (invocation.getArgs().length > 1) {
			parameter = invocation.getArgs()[1];
		}

		BoundSql boundSql = mappedStatement.getBoundSql(parameter);
		Configuration configuration = mappedStatement.getConfiguration();
		return sqlResolve(configuration, boundSql);
	}

	protected Object doFinish(Invocation invocation, Transaction t)
			throws InvocationTargetException, IllegalAccessException {
		Object returnObj = null;
		try {
			returnObj = invocation.proceed();
			t.setStatus(Transaction.SUCCESS);
		} catch (Exception e) {
			t.setStatus(e);			
			Cat.logError(e);
			throw e;
		} finally {
			t.complete();
		}

		return returnObj;
	}

	protected String getSQLDatabaseUrlByStatement(MappedStatement mappedStatement) {
		String url = null;
		DataSource dataSource = null;
		try {
			Configuration configuration = mappedStatement.getConfiguration();
			Environment environment = configuration.getEnvironment();
			dataSource = environment.getDataSource();

			url = switchDataSource(dataSource);

			return url;
		} catch (NoSuchFieldException | IllegalAccessException | NullPointerException e) {
			Cat.logError(e);
		}

		Cat.logError(new Exception(
				"UnSupport type of DataSource : " + (null == dataSource ? "NULL" : dataSource.getClass().toString())));

		return MYSQL_DEFAULT_URL;
	}

	protected String switchDataSource(DataSource dataSource) throws NoSuchFieldException, IllegalAccessException {
		String url = null;
		
		if (dataSource.getClass().getName()
				.equalsIgnoreCase("com.baomidou.dynamic.datasource.DynamicRoutingDataSource")) {
			dataSource = ((com.baomidou.dynamic.datasource.DynamicRoutingDataSource) dataSource).determineDataSource();
			if (dataSource instanceof com.baomidou.dynamic.datasource.ds.ItemDataSource) {
				dataSource = ((com.baomidou.dynamic.datasource.ds.ItemDataSource) dataSource).getRealDataSource();
			}
		}		

		if (dataSource instanceof PooledDataSource) {
			Field dataSource1 = dataSource.getClass().getDeclaredField("dataSource");
			dataSource1.setAccessible(true);
			UnpooledDataSource dataSource2 = (UnpooledDataSource) dataSource1.get(dataSource);
			url = dataSource2.getUrl();
		} else if (dataSource.getClass().getSimpleName().contains("HikariDataSource")) {
			url = ((HikariDataSource) dataSource).getJdbcUrl();
		} else if (dataSource.getClass().getSimpleName().contains("DruidDataSource")) {
			url = ((DruidDataSource) dataSource).getUrl();
		}

		return url;
	}

	public String sqlResolve(Configuration configuration, BoundSql boundSql) {
		Object parameterObject = boundSql.getParameterObject();
		List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
		String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
		if (!parameterMappings.isEmpty() && parameterObject != null) {
			TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
			if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
				sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(resolveParameterValue(parameterObject)));

			} else {
				MetaObject metaObject = configuration.newMetaObject(parameterObject);
				Matcher matcher = PARAMETER_PATTERN.matcher(sql);
				StringBuffer sqlBuffer = new StringBuffer();
				for (ParameterMapping parameterMapping : parameterMappings) {
					String propertyName = parameterMapping.getProperty();
					Object obj = null;
					if (metaObject.hasGetter(propertyName)) {
						obj = metaObject.getValue(propertyName);
					} else if (boundSql.hasAdditionalParameter(propertyName)) {
						obj = boundSql.getAdditionalParameter(propertyName);
					}
					if (matcher.find()) {
						matcher.appendReplacement(sqlBuffer, Matcher.quoteReplacement(resolveParameterValue(obj)));
					}
				}
				matcher.appendTail(sqlBuffer);
				sql = sqlBuffer.toString();
			}
		}
		return sql;
	}

	private String resolveParameterValue(Object obj) {
		String value = null;
		if (obj instanceof String) {
			value = "'" + obj.toString() + "'";
		} else if (obj instanceof Date) {
			DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
			value = "'" + formatter.format((Date) obj) + "'";
		} else {
			if (obj != null) {
				value = obj.toString();
			} else {
				value = "";
			}

		}
		return value;
	}

	@Override
	public Object plugin(Object target) {
		if (target instanceof Executor) {
			return Plugin.wrap(target, this);
		}
		return target;
	}

	@Override
	public void setProperties(Properties properties) {
	}

}